const tty = require('tty')

if (!tty.getWindowSize) {
  // this is really old method, long removed from Node, but Mocha
  // reporters fall back on it if they cannot use `process.stdout.getWindowSize`
  // we need to polyfill it.
  tty.getWindowSize = () => [40, 80]
}

const { parentPort, workerData } = require('worker_threads')
const event = require('../../event')
const container = require('../../container')
const { getConfig } = require('../utils')
const { tryOrDefault, deepMerge } = require('../../utils')

let stdout = ''

const stderr = ''

// Requiring of Codecept need to be after tty.getWindowSize is available.
const Codecept = require(process.env.CODECEPT_CLASS_PATH || '../../codecept')

const { options, tests, testRoot, workerIndex, poolMode } = workerData

// hide worker output
// In pool mode, only suppress output if debug is NOT enabled
// In regular mode, hide result output but allow step output in verbose/debug
if (poolMode && !options.debug) {
  // In pool mode without debug, suppress only result summaries and failures, but allow Scenario Steps
  const originalWrite = process.stdout.write
  process.stdout.write = string => {
    // Always allow Scenario Steps output (including the circle symbol)
    if (string.includes('Scenario Steps:') || string.includes('◯ Scenario Steps:')) {
      return originalWrite.call(process.stdout, string)
    }
    if (string.includes('  FAIL  |') || string.includes('  OK  |') || string.includes('-- FAILURES:') || string.includes('AssertionError:') || string.includes('◯ File:')) {
      return true
    }
    return originalWrite.call(process.stdout, string)
  }
} else if (!poolMode && !options.debug && !options.verbose) {
  process.stdout.write = string => {
    stdout += string
    return true
  }
} else {
  // In verbose/debug mode for test/suite modes, show step details
  // but suppress individual worker result summaries to avoid duplicate output
  const originalWrite = process.stdout.write
  const originalConsoleLog = console.log

  process.stdout.write = string => {
    // Suppress individual worker result summaries and failure reports
    if (string.includes('  FAIL  |') || string.includes('  OK  |') || string.includes('-- FAILURES:') || string.includes('AssertionError:') || string.includes('◯ File:') || string.includes('◯ Scenario Steps:')) {
      return true
    }
    return originalWrite.call(process.stdout, string)
  }

  // Override console.log to catch result summaries
  console.log = (...args) => {
    const fullMessage = args.join(' ')
    if (fullMessage.includes('  FAIL  |') || fullMessage.includes('  OK  |') || fullMessage.includes('-- FAILURES:')) {
      return
    }
    return originalConsoleLog.apply(console, args)
  }
}

const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})

// important deep merge so dynamic things e.g. functions on config are not overridden
const config = deepMerge(getConfig(options.config || testRoot), overrideConfigs)

// Load test and run
const codecept = new Codecept(config, options)
codecept.init(testRoot)
codecept.loadTests()
const mocha = container.mocha()

if (poolMode) {
  // In pool mode, don't filter tests upfront - wait for assignments
  // We'll reload test files fresh for each test request
} else {
  // Legacy mode - filter tests upfront
  filterTests()
}

// run tests
;(async function () {
  if (poolMode) {
    await runPoolTests()
  } else if (mocha.suite.total()) {
    await runTests()
  }
})()

let globalStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }

async function runTests() {
  try {
    await codecept.bootstrap()
  } catch (err) {
    throw new Error(`Error while running bootstrap file :${err}`)
  }
  listenToParentThread()
  initializeListeners()
  disablePause()
  try {
    await codecept.run()
  } finally {
    await codecept.teardown()
  }
}

async function runPoolTests() {
  try {
    await codecept.bootstrap()
  } catch (err) {
    throw new Error(`Error while running bootstrap file :${err}`)
  }

  initializeListeners()
  disablePause()

  // Accumulate results across all tests in pool mode
  let consolidatedStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
  let allTests = []
  let allFailures = []
  let previousStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }

  // Keep requesting tests until no more available
  while (true) {
    // Request a test assignment
    sendToParentThread({ type: 'REQUEST_TEST', workerIndex })

    const testResult = await new Promise((resolve, reject) => {
      // Set up pool mode message handler
      const messageHandler = async eventData => {
        if (eventData.type === 'TEST_ASSIGNED') {
          const testUid = eventData.test

          try {
            // In pool mode, we need to create a fresh Mocha instance for each test
            // because Mocha instances become disposed after running tests
            container.createMocha() // Create fresh Mocha instance
            filterTestById(testUid)
            const mocha = container.mocha()

            if (mocha.suite.total() > 0) {
              // Run the test and complete
              await codecept.run()

              // Get the results from this specific test run
              const result = container.result()
              const currentStats = result.stats || {}

              // Calculate the difference from previous accumulated stats
              const newPasses = Math.max(0, (currentStats.passes || 0) - previousStats.passes)
              const newFailures = Math.max(0, (currentStats.failures || 0) - previousStats.failures)
              const newTests = Math.max(0, (currentStats.tests || 0) - previousStats.tests)
              const newPending = Math.max(0, (currentStats.pending || 0) - previousStats.pending)
              const newFailedHooks = Math.max(0, (currentStats.failedHooks || 0) - previousStats.failedHooks)

              // Add only the new results
              consolidatedStats.passes += newPasses
              consolidatedStats.failures += newFailures
              consolidatedStats.tests += newTests
              consolidatedStats.pending += newPending
              consolidatedStats.failedHooks += newFailedHooks

              // Update previous stats for next comparison
              previousStats = { ...currentStats }

              // Add new failures to consolidated collections
              if (result.failures && result.failures.length > allFailures.length) {
                const newFailures = result.failures.slice(allFailures.length)
                allFailures.push(...newFailures)
              }
            }

            // Signal test completed and request next
            parentPort?.off('message', messageHandler)
            resolve('TEST_COMPLETED')
          } catch (err) {
            parentPort?.off('message', messageHandler)
            reject(err)
          }
        } else if (eventData.type === 'NO_MORE_TESTS') {
          // No tests available, exit worker
          parentPort?.off('message', messageHandler)
          resolve('NO_MORE_TESTS')
        } else {
          // Handle other message types (support messages, etc.)
          container.append({ support: eventData.data })
        }
      }

      parentPort?.on('message', messageHandler)
    })

    // Exit if no more tests
    if (testResult === 'NO_MORE_TESTS') {
      break
    }
  }

  try {
    await codecept.teardown()
  } catch (err) {
    // Log teardown errors but don't fail
    console.error('Teardown error:', err)
  }

  // Send final consolidated results for the entire worker
  const finalResult = {
    hasFailed: consolidatedStats.failures > 0,
    stats: consolidatedStats,
    duration: 0, // Pool mode doesn't track duration per worker
    tests: [], // Keep tests empty to avoid serialization issues - stats are sufficient
    failures: allFailures, // Include all failures for error reporting
  }

  sendToParentThread({ event: event.all.after, workerIndex, data: finalResult })
  sendToParentThread({ event: event.all.result, workerIndex, data: finalResult })

  // Add longer delay to ensure messages are delivered before closing
  await new Promise(resolve => setTimeout(resolve, 100))

  // Close worker thread when pool mode is complete
  parentPort?.close()
}

function filterTestById(testUid) {
  // Reload test files fresh for each test in pool mode
  const files = codecept.testFiles

  // Get the existing mocha instance
  const mocha = container.mocha()

  // Clear suites and tests but preserve other mocha settings
  mocha.suite.suites = []
  mocha.suite.tests = []

  // Clear require cache for test files to ensure fresh loading
  files.forEach(file => {
    delete require.cache[require.resolve(file)]
  })

  // Set files and load them
  mocha.files = files
  mocha.loadFiles()

  // Now filter to only the target test - use a more robust approach
  let foundTest = false
  for (const suite of mocha.suite.suites) {
    const originalTests = [...suite.tests]
    suite.tests = []

    for (const test of originalTests) {
      if (test.uid === testUid) {
        suite.tests.push(test)
        foundTest = true
        break // Only add one matching test
      }
    }

    // If no tests found in this suite, remove it
    if (suite.tests.length === 0) {
      suite.parent.suites = suite.parent.suites.filter(s => s !== suite)
    }
  }

  // Filter out empty suites from the root
  mocha.suite.suites = mocha.suite.suites.filter(suite => suite.tests.length > 0)

  if (!foundTest) {
    // If testUid doesn't match, maybe it's a simple test name - try fallback
    mocha.suite.suites = []
    mocha.suite.tests = []
    mocha.loadFiles()

    // Try matching by title
    for (const suite of mocha.suite.suites) {
      const originalTests = [...suite.tests]
      suite.tests = []

      for (const test of originalTests) {
        if (test.title === testUid || test.fullTitle() === testUid || test.uid === testUid) {
          suite.tests.push(test)
          foundTest = true
          break
        }
      }
    }

    // Clean up empty suites again
    mocha.suite.suites = mocha.suite.suites.filter(suite => suite.tests.length > 0)
  }
}

function filterTests() {
  const files = codecept.testFiles
  mocha.files = files
  mocha.loadFiles()

  for (const suite of mocha.suite.suites) {
    suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
  }
}

function initializeListeners() {
  // suite
  event.dispatcher.on(event.suite.before, suite => sendToParentThread({ event: event.suite.before, workerIndex, data: suite.simplify() }))
  event.dispatcher.on(event.suite.after, suite => sendToParentThread({ event: event.suite.after, workerIndex, data: suite.simplify() }))

  // calculate duration
  event.dispatcher.on(event.test.started, test => (test.start = new Date()))

  // tests
  event.dispatcher.on(event.test.before, test => sendToParentThread({ event: event.test.before, workerIndex, data: test.simplify() }))
  event.dispatcher.on(event.test.after, test => sendToParentThread({ event: event.test.after, workerIndex, data: test.simplify() }))
  // we should force-send correct errors to prevent race condition
  event.dispatcher.on(event.test.finished, (test, err) => sendToParentThread({ event: event.test.finished, workerIndex, data: { ...test.simplify(), err } }))
  event.dispatcher.on(event.test.failed, (test, err) => sendToParentThread({ event: event.test.failed, workerIndex, data: { ...test.simplify(), err } }))
  event.dispatcher.on(event.test.passed, (test, err) => sendToParentThread({ event: event.test.passed, workerIndex, data: { ...test.simplify(), err } }))
  event.dispatcher.on(event.test.started, test => sendToParentThread({ event: event.test.started, workerIndex, data: test.simplify() }))
  event.dispatcher.on(event.test.skipped, test => sendToParentThread({ event: event.test.skipped, workerIndex, data: test.simplify() }))

  // steps
  event.dispatcher.on(event.step.finished, step => sendToParentThread({ event: event.step.finished, workerIndex, data: step.simplify() }))
  event.dispatcher.on(event.step.started, step => sendToParentThread({ event: event.step.started, workerIndex, data: step.simplify() }))
  event.dispatcher.on(event.step.passed, step => sendToParentThread({ event: event.step.passed, workerIndex, data: step.simplify() }))
  event.dispatcher.on(event.step.failed, step => sendToParentThread({ event: event.step.failed, workerIndex, data: step.simplify() }))

  event.dispatcher.on(event.hook.failed, (hook, err) => sendToParentThread({ event: event.hook.failed, workerIndex, data: { ...hook.simplify(), err } }))
  event.dispatcher.on(event.hook.passed, hook => sendToParentThread({ event: event.hook.passed, workerIndex, data: hook.simplify() }))
  event.dispatcher.on(event.hook.finished, hook => sendToParentThread({ event: event.hook.finished, workerIndex, data: hook.simplify() }))

  if (!poolMode) {
    // In regular mode, close worker after all tests are complete
    event.dispatcher.once(event.all.after, () => {
      sendToParentThread({ event: event.all.after, workerIndex, data: container.result().simplify() })
    })
    // all
    event.dispatcher.once(event.all.result, () => {
      sendToParentThread({ event: event.all.result, workerIndex, data: container.result().simplify() })
      parentPort?.close()
    })
  } else {
    // In pool mode, don't send result events for individual tests
    // Results will be sent once when the worker completes all tests
  }
}

function disablePause() {
  global.pause = () => {}
}

function sendToParentThread(data) {
  parentPort?.postMessage(data)
}

function listenToParentThread() {
  if (!poolMode) {
    parentPort?.on('message', eventData => {
      container.append({ support: eventData.data })
    })
  }
  // In pool mode, message handling is done in runPoolTests()
}
